React Native in 24 Hours


Posted by huli on 2016-11-10

React Native in 24 Hours

前言

上個禮拜的時候,我們公司舉辦了一年一度的黑客松,一隊有四個人。因為我才剛加入公司大概兩個禮拜而已,所以也沒認識什麼人。不過,剛好當初找我進來的同事問我要不要一起參加,就跟著報名了。

黑客松的時間是禮拜五早上十一點到禮拜六同一時間,一共 24 個小時。我的三個隊友,一個是 PM、一個是資安部門、一個是 SA(System Admin) 部門。因此,在他們知道我現在是前端工程師,以前是 Android 工程師以後,理所當然地,Mobile App 的部分就由我負責了。

這也是這篇標題的由來:React Native in 24 hours,想跟大家分享在 24 小時之內,可以用 React Native 做出什麼樣的作品,以及過程中碰到的困難及挑戰。

Idea

先簡單談一下我們這組那時候想做的東西,是一個 Password generator and manager。

大家都知道,記憶密碼是一件很麻煩的事情,所以,很多人會在每個網站都用同一組密碼。而大家也知道,這樣的壞處就是,當你其中一個網站的密碼外洩以後,其他網站也會跟著遭殃,這是一件滿可怕的事情。

因此有很多人會用密碼管理服務,只要記住這個服務的密碼,其他密碼都靠這個網站幫你儲存。可是,我的隊友覺得這樣依然會有問題,那就是這個網站仍然有安全性的風險。有沒有可能完全不儲存密碼呢?

有!那就是不要用儲存的,而是用「產生」的,只要有一套邏輯負責產生密碼,保證輸入一樣,輸出也一樣,就可以利用這一套邏輯每一次都產生密碼。

舉個例子,假設這套邏輯是

password = sha_256(root_password + domain_name + 'I_AM_WEEK_SALT')

這樣你只要有你的主密碼(root_password)跟你要登入的網站,就能幫你產生出一組獨特的密碼。

這就是這個產品的核心概念。

不過,上面那套邏輯有個問題是:假如我某個網站密碼外洩了,我想換一個,怎麼換?
這時候又要引進一個新的參數叫做 changeID,是一個數字,把密碼產生的邏輯變成

password = sha_256(root_password + domain_name + changeID + 'I_AM_WEEK_SALT')

就可以任意更換無限次密碼。不過其實這樣,也頂多就是一個密碼生成器。
當要加上「管理」的功能時,就比較麻煩了,例如說我們想要幫使用者儲存他的帳號,就可以自動填入。

因此我們就要有後端的 DB 去記錄這些資料,並且要有登入機制,而且一旦引入登入機制,就違反了「不想儲存任何密碼」的這個初衷。
總之,在討論一些 feature 要不要做的時候,花了很多時間,討論了很久。

因為這些內容其實有點瑣碎,因此我就不多提了,如果你對這個概念有興趣,可以參考 LessPass。這跟我隊友想的 idea 有 87% 像,而且做得很完整,可以參考看看。

說好的 React Native 呢?

好了,大概介紹完產品之後,終於要進入到 React Native 的部分。因為我之前有寫過 React + Redux 的網站,也有 run 過一次 React Native 的簡單範例(Hello World),所以對 React Native 不算陌生,至少能 build 的起來!

就讓我們先從這個 App 的第一頁開始吧!

React Native in 24 Hours

這一頁很簡單,就是一張大張背景圖,上面放 logo 跟一些字,再加兩個按鈕跟一個可以點的「Join Now」。其實以前在寫 Android 的時候,最麻煩的不是程式碼,而是版面!直到現在,我還是覺得自己跟 Android 的 Layout 很不熟,沒辦法排出自己想要的版面。

但是現在我們有了 React Native,有了新的排版方式:flexbox。

如果你不知道什麼是 flexbox,我可以很簡單的介紹一下,基本上就是你可以選擇你要直的排還是橫的排、要靠上靠下對齊還是置中、空隙應該怎麼分配、每個版面佔多少比例等等。我自己覺得是個滿直覺的排版方式。

以上面那個版面為例,就可以用直的來排,然後按照比例分成四塊,按鈕那個區塊內部用橫的排,然後切對半:

React Native in 24 Hours

其實這就很像 React 用 component 來切的概念,切成很多很多細小的組件再合在一起。

我當初寫 code 的時候,都把這個分頁開在旁邊,是很淺顯易懂的圖片說明,可以很方便的就找到自己想要的排版方式應該怎麼用。

至於在按鈕的部分,我是用 APSL/react-native-button 這個第三方的套件,但詳細原因我也忘記了,可能是用原生的碰到什麼問題,所以去找了第三方的來用。

這邊附上最後寫出來的部分程式碼:

  render() {
    return (
      <View style={styles.container}>
        <View style={styles.bgImageWrapper}>
          <Image source={require('..https://static.coderbridge.com/img/techbridge/images/bg.png')} style={styles.bg} />
        </View>

        <Image source={require('..https://static.coderbridge.com/img/techbridge/images/logo-white.png')} style={styles.image}/>
        <Text style={styles.text}>Only you have your password</Text>
        <View style={styles.btnGroup}>
          <Button style={styles.btnGuest} textStyle={{fontSize: 18, color: 'white'}} onPress={this.onGuestClick.bind(this)}>
            Use as guest
          </Button>

          <Button style={styles.btnSignIn} textStyle={{fontSize: 18, color: 'white'}} onPress={this.onSignInClick.bind(this)}>
            Sign in
          </Button>
        </View>
        <Text style={styles.textJoin} onPress={this.onJoinClick.bind(this)}>Join now</Text>

      </View>
    );
  }

這邊有兩點要提一下,第一點是 textStyle 的部分最好也先宣告成變數再使用,但因為是黑客松所以比較少時間去做這些調整,就比較不好的直接這樣寫了。第二點是this.onGuestClick.bind(this)這樣的方式其實不好,但不知道為什麼,我沒辦法在constructor裡面先bind,所以只好這樣寫了。

其實只要能把這個 render 的函式寫出來,這一頁就看起來有模有樣了,可是問題是,我們還有其他很多頁。要怎麼切換到別的畫面呢?

Navigator

如果要在多個畫面之間切換,就要靠Navigator這個組件了。從官方文件可以大致看出用法,或是可以參考別人寫的教學

直接附上程式碼再來解釋

import React, { Component } from 'react';
import {
  Navigator
} from 'react-native';

import MainScene from './scenes/MainScene';

export default class Zeropass extends Component {
  render() {
    var defaultName = "MainScene";
    var defaultComp = MainScene;

    return (
      <Navigator
        initialRoute={{
          name: defaultName,
          component: defaultComp
        }}
        configureScene={(route) => {
          return Navigator.SceneConfigs.PushFromRight;
        }}
        renderScene={(route, navigator) => {
          var Component = route.component;
          return (
            <Component {...route.params} navigator={navigator} />
          );
        }}
      />
    )
  }
}

initialRoute就是一開始的時候要給什麼參數,可以想成是 defaultState 的感覺,configureScene是跟換場動畫有關係的,這邊直接用內建的PushFromRight,會有一個還不錯的從右邊推進來的效果。

renderScene則是精華所在,就是Navigatorrender函式,說明應該要渲染出什麼東西。

這邊先取route.component,這個component對應到的就是我在initialRoute這邊給的component這個 key,之後如果要換頁的話也要用這個 key 帶東西進來。

並且傳入navigator讓組件可以呼叫,以及加上...route.params,就可以帶額外的參數進來。

這只是最基礎的架構而已,但實際上如果要換頁,應該要怎麼寫呢?

  toNext() {
    const { navigator } = this.props;
    if(navigator) {
      navigator.push({
        name: 'GuestLoginScene',
        component: GuestLoginScene,
      })
    }
  }

因為在renderScene的時候,我們有把navigator傳進去,所以在每一個 component 都可以用 this.props 取出來,想要換頁的話就只要 push 一個物件進去就好了。物件的格式自己決定好就行了。所以你會發現這邊的格式跟剛剛initialRoute傳進去的格式一模一樣。

好了,所以第一頁完成了、換頁的問題也解決了。剩下的就是把其它頁面刻出來,好像就差不多了。前途真是一片光明璀璨,看來 24 小時太多了。

等等,可是我們還有 Tab 啊

React Native in 24 Hours

當初在做這個頁面的時候,原本的設計是 DrawerLayout,就是 Android 風格的從左邊滑出來的列表。可是因為在做這個 App 時想要跨平台通吃,所以 Drawer 方案可見是行不通的。況且,我看了一下官方文件,實作上感覺也沒那麼簡單。

因此,最後找了第三方套件的react-native-tab-view來用。因為無論在 iOS 或是在 Android,都可以看到 Tab 的出現,所以會比 Drawer 更好一些。

這個 Tabview 的用法相當簡單,客製化程度滿高的,只要自己實作幾個函式就好。

const icons = {
  account: require('..https://static.coderbridge.com/img/techbridge/images/icon_account.png'),
  edit: require('..https://static.coderbridge.com/img/techbridge/images/icon_edit.png'),
  setting: require('..https://static.coderbridge.com/img/techbridge/images/icon_setting.png'),
  help: require('..https://static.coderbridge.com/img/techbridge/images/icon_help.png'),
}

export default class TabViewExample extends Component {

  constructor(props) {
    super(props);

  }

  state = {
    index: 0,
    routes: [
      { key: 'account', title: 'Account', icon: 'account'},
      { key: 'new', title: 'Create', icon: 'edit' },
      { key: 'setting', title: 'Setting', icon: 'setting' },
      { key: 'help', title: 'Help', icon: 'help' },
    ],
  };

  _handleChangeTab = (index) => {
    this.setState({ index });
  };

  _renderIcon = ({ route }) => {
    return (
      <Image
        source={icons[route.icon]}
        style={styles.icon}
        color='white'
      />
    );
  };

  _renderHeader = (props) => {
    return (
      <TabBar 
        renderIcon={this._renderIcon}
        tabStyle={styles.tab} 
        labelStyle={styles.label}
        {...props} />
    );
  };

  _renderScene = ({ route }) => {
    console.log(route.key);
    switch (route.key) {

      case 'account':
        return <AccountTab />;
      case 'new':
        return <CreateTab />;
      case 'help':
        return <HelpTab />;
      case 'setting':
        return <SettingTab />;
      default:
        return null;
    }
  };

  render() {
    return (
        <TabViewAnimated
          style={styles.container}
          navigationState={this.state}
          renderScene={this._renderScene}
          renderFooter={this._renderHeader}
          onRequestChangeTab={this._handleChangeTab}
        />
    );
  }
}

Navigator其實有異曲同工之妙,都是靠renderScene這個函式去決定如何渲染出畫面。在這邊就很簡單的根據目前所選到的 key 去渲染出相對應的組件即可。

可是,假如 Tab 的頁面也是巢狀的呢?例如說我第一個 Tab 可能是輸入密碼的畫面,輸入完按下確定之後話跳到下一個畫面,這個要怎麼做呢?

我自己猜應該是也可以用navigator來做,在renderScene的時候渲染出navigator,其他的就跟我們剛開頭介紹的差不多。

意思就是,每一個 Tab 其實都像是一個小的 App,有自己的navigator來管理自己的狀態。

但是在黑客松的時候,我一時半刻沒有想到這樣的解法,於是就手刻了一個最直覺、最暴力的。

import SiteScene from './SiteScene';
import PasswordScene from './PasswordScene';

export default class CreateTab extends Component {

  constructor(props) {
    super(props);

    // 2 page
    this.state = {
      page: 1,
      site: ''
    }
  }

  goBack() {
    this.setState({
      page: 1
    })
  }

  toNext(site) {
    this.setState({
      ...this.state,
      page: 2,
      site
    })
  }

  render() {

    const { page, site } = this.state;

    return (
        <View style={styles.container}>
          <View style={styles.top}>
            <Text style={styles.back} onPress={this.goBack.bind(this)}>
              { page==2 ? ' ← ' : ' ' }
            </Text>
          </View>
          {page==1 && <SiteScene toNext={this.toNext.bind(this)}/>}
          {page==2 && <PasswordScene site={site}/>}
        </View>
    );
  }
};

在 tab 裡面用 state 來管理目前的 index,因為只有兩個頁面所以還滿好做的,根據 index 渲染出相對應的頁面即可。然後還可以用一個簡單的 navbar 包起來,就可以點上面的箭頭回到上一頁。

React Native in 24 Hours

React Native in 24 Hours

就這樣,tab 的問題也解決了。看起來一切都 work 的不錯,可以自由自在在各個頁面之間切換自如了。
接下來,好像就只剩下串 API 了。

API

因為有支援 async/await 語法,所以寫起來十分清爽。下面這一段程式碼是要去 server 抓取使用者儲存過資訊的 domain 回來。
為了方便起見,我把所有的 API call 都包在API這個檔案裡面,用fetch去拿資料。

const API_URL = {
    'getInfo': 'http://api.com/api/v1/users',
}

const API = {

  getInfo: async function(uid) {

    let response = await fetch(`${API_URL.getInfo}/${uid}`);
    let json = await response.json();

    return json;
  }
}

export default API;
async componentDidMount() {

  let domains = [];
  this.setState({
    spinnerShow: true
  });

  try {
    const response = await API.getInfo(store.getId());
    domains = response.domains;
  } catch(err) {
    console.log(err);
  }

  this.setState({
    dataSource: ds.cloneWithRows(domains),
    spinnerShow: false
  })
}

(不過我後來想一下,這樣子把componentDidMount直接包成 async 好像不太好,應該獨立成一個函式去呼叫)

體驗跨平台的威力

因為我自己是用 Android 的,所以我在開發的時候都是直接用 Android 實機測試。不得不提已經被講到爛掉的,React Native 的 hot reload,用起來真的很爽快,只要儲存檔案之後就可以在 App 上看到新的畫面。(雖然原生的 Android 現在也支援了,但我還沒有機會體驗到就是了)

其實原本我的隊友只有預期我開發出 Android 的版本(他們都用 iPhone),但殊不知 React Native 太過強大,當我把 App 開發到九成的時候,想說來試試看 build 到 iOS 上好了,發現跟我想像中的不一樣。

我以為要 build 的時候應該會碰到很多錯誤然後要慢慢修,或者是沒有 Apple 的開發者帳號就沒辦法 build 到手機上之類的。

但我完完全全錯了。React Native 就是那麼簡單,超級輕鬆就可以有一個 iOS 的版本。也根本不用什麼 Apple 開發者帳號,就可以安裝到手機上面測試(只是要開一些安全性設定就是了)。

在 build 的時候是有碰到一點小問題,但拜過 Google 大神之後基本上就解決了。

(不過會那麼輕鬆,也是因為我基本上沒用到 Native 的功能,排版也是兩個平台都長一模一樣,所以可以共用 100% 的程式碼。)

除了 iOS 很輕鬆以外,Android 如果要產生可以上 Google play 的安裝包也很容易,官方教學寫的超級清楚,就參數填一填以後打個指令,剩下的全部都幫你做好。

結論

這篇文章主要是想分享一下當初碰到的困難以及開發的歷程,花最久時間的是排版(因為對 flexbox 沒那麼熟,所以不知道排出來會是怎樣),還有 Tab 那一段也花了一點時間研究作法。但總體來說,我覺得 React Native 的開發效率是很高的。

因為我中間有睡覺,所以實際上 coding 應該 18 個小時左右,就可以做出一個可以動、有接 API 又跨平台的 App。意思就是說如果你的 App 沒有很複雜的話,基本上是可以在兩三天之內就做出一個還不錯的 prototype。

只要掌握幾個元素:View, Button, Image, Navigator, ListView, TextInput,差不多就可以完成 80% 的工作了。

我最喜歡的還是它的排版方式,對前端工程師來說比較熟悉。開發環境的設置我覺得也比 Native 的簡單許多,而且可以用 JavaScript 我覺得也是一大好處。

之前看了那麼多 React Native 的介紹文,我覺得最快速能認識的方式還是自己跳下去寫 code。如果大家假日有空的話,也可以試試看來場自己一個人的黑客松,挑戰在 24 小時以內用 React Native 做出一個簡單的 App,相信一定會很有收穫的。

最後,附上這個 App 的 Github:zeropass-react-native
如果有興趣的話可以參考看看
(因為時間緊迫,所以很多東西都是只求能動就好,很多都是錯誤示範,真的只是「僅供參考」)

關於作者:
@huli 野生工程師,相信分享與交流能讓世界變得更美好


#React #react_native #Android









Related Posts

Jest "Cannot find module from xxx" issue

Jest "Cannot find module from xxx" issue

沒有高低之分 只有先後順序

沒有高低之分 只有先後順序

JS30 Day 23 筆記

JS30 Day 23 筆記




Newsletter




Comments